package com.basker.pisces.core;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;

import org.springframework.core.NamedThreadLocal;
import org.springframework.util.StringUtils;

import com.basker.pisces.core.enums.DataObjectState;
import com.basker.pisces.core.helper.DataContractHelper;
import com.basker.pisces.core.meta.IDataObjectMeta;
import com.basker.pisces.core.meta.IField;
import com.basker.pisces.core.meta.bean.BeanCollectionField;
import com.basker.pisces.core.meta.bean.BeanDataField;
import com.basker.pisces.core.meta.bean.BeanMetaManager;
import com.basker.pisces.exception.ExceptionFactory;
import com.basker.pisces.utils.ClassUtils;

import javassist.CannotCompileException;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.Modifier;
import javassist.NotFoundException;

public class DataContractGenerator {
    private static final ThreadLocal<Boolean> DATACONTRACT_GENERATOR_ENABLE = new NamedThreadLocal<>(
            "DataContract generator enable");

    private static final String DATACONTRACT_CLASS_SUFFIX = "$DataContract";

    /**
     * 禁用增强对象，这是一个线程级的开关
     */
    public static void disable() {
        DATACONTRACT_GENERATOR_ENABLE.remove();
    }

    /**
     * 启用增强对象，这是一个线程级的开关
     */
    public static void enable() {
        DATACONTRACT_GENERATOR_ENABLE.set(Boolean.TRUE);
    }

    public static boolean isDataContractClass(Class<?> cls) {
        return IDataContract.class.isAssignableFrom(cls);
    }

    /**
     * 增强对象是否开启，这是一个线程级的状态
     *
     * @return
     */
    public static boolean isEnable() {
        Boolean enable = DATACONTRACT_GENERATOR_ENABLE.get();
        return enable != null && enable;
    }

    private Class<?> beanClass;

    public DataContractGenerator(Class<?> beanClass) {
        this.beanClass = beanClass;
    }

    public Class<?> getDataContractClass() {
        ClassPool classPool = ClassPool.getDefault();
        String className = this.getDataContractClassName();

        ClassLoader clsLoader = this.beanClass.getClassLoader();
        Class<?> retClass = ClassUtils.getClass(className, clsLoader, false);

        if (retClass != null) {
            return retClass;
        }

        CtClass ctClass = null;

        try {
            ctClass = classPool.get(className);
        } catch (NotFoundException e) {
            try {
                ctClass = classPool.makeClass(className);
                enhanceClass(classPool, ctClass);
            } catch (CannotCompileException | NotFoundException e1) {
                if (ctClass != null) {
                    ctClass.detach();
                }
                throw ExceptionFactory.createRuntimeException(e1);
            }
        }

        try {
            Class<?> clazz = ctClass.toClass(clsLoader, null);
            return clazz;

        } catch (CannotCompileException e) {
            throw ExceptionFactory.createRuntimeException(e);
        }
    }

    private void addField(ClassPool classPool, CtClass ctClass, Class<?> fieldClass, String fieldName,
                          boolean addGetSet) throws CannotCompileException, NotFoundException {
        CtField field = new CtField(classPool.get(fieldClass.getName()), fieldName, ctClass);
        field.setModifiers(Modifier.PRIVATE | Modifier.TRANSIENT | Modifier.VOLATILE);

        ctClass.addField(field);

        if (addGetSet) {
            fieldName = StringUtils.capitalize(fieldName);
            String get = "get";
            String set = "set";

            if (fieldClass.equals(Boolean.class) || fieldClass.equals(boolean.class)) {
                get = "is";
            }

            ctClass.addMethod(CtNewMethod.setter(set + fieldName, field));
            ctClass.addMethod(CtNewMethod.getter(get + fieldName, field));
        }
    }

    private void addFields(ClassPool classPool, CtClass ctClass) throws CannotCompileException, NotFoundException {
        this.addField(classPool, ctClass, boolean.class, "fromDB", true);
        this.addField(classPool, ctClass, DataObjectState.class, "state", true);
        this.addField(classPool, ctClass, boolean.class, "readOnly", true);
        this.addField(classPool, ctClass, int.class, "initFlag", false);
        this.addField(classPool, ctClass, Map.class, "oldValues", false);
        this.addField(classPool, ctClass, Map.class, "fromDBMap", false);
    }

    private void addMethod(ClassPool classPool, CtClass ctClass, CtClass returnType, String methodName,
                           CtClass[] parameters, String body) throws CannotCompileException {
        CtMethod method = new CtMethod(returnType, methodName, parameters, ctClass);
        method.setModifiers(Modifier.PUBLIC);
        method.setBody(body);
        ctClass.addMethod(method);

    }

    private void addMethods(ClassPool classPool, CtClass ctClass) throws CannotCompileException, NotFoundException {
        CtClass stringType = classPool.get(String.class.getName());
        CtClass objectType = classPool.get(Object.class.getName());

        this.addMethod(classPool, ctClass, CtClass.voidType, "beginInit", null, "{initFlag++;}");
        this.addMethod(classPool, ctClass, CtClass.voidType, "endInit", null, "{initFlag--;}");
        this.addMethod(classPool, ctClass, CtClass.booleanType, "isInInitial", null, "{return initFlag>0;}");

        this.addMethod(classPool, ctClass, CtClass.voidType, "recordOldValue", new CtClass[]{stringType, objectType},
                "{if($0.oldValues==null){$0.oldValues = new java.util.HashMap();}$0.oldValues.put($1, $2);}");

        this.addMethod(classPool, ctClass, CtClass.voidType, "clearOldValues", null,
                "{if($0.oldValues!=null){$0.oldValues.clear();}}");

        this.addMethod(classPool, ctClass, CtClass.booleanType, "isChanged", new CtClass[]{stringType},
                "{return $0.oldValues==null?false:$0.oldValues.containsKey($1);}");

        this.addMethod(classPool, ctClass, CtClass.voidType, "setFromDB",
                new CtClass[]{stringType, CtClass.booleanType},
                "{if($0.fromDBMap==null){$0.fromDBMap = new java.util.HashMap();}$0.fromDBMap.put($1,Boolean.valueOf($2));}");

        this.addMethod(classPool, ctClass, CtClass.booleanType, "isFromDB", new CtClass[]{stringType},
                "{if($0.fromDBMap==null){return false;}return Boolean.parseBoolean(String.valueOf($0.fromDBMap.get($1)));}");

        this.addMethod(classPool, ctClass, classPool.get("java.lang.Object"), "getOldValue",
                new CtClass[]{stringType}, "{return $0.oldValues==null?null:$0.oldValues.get($1);}");
    }

    private CtClass enhanceClass(ClassPool classPool, CtClass ctClass)
            throws CannotCompileException, NotFoundException {
        ClassClassPath beanClassPath = new ClassClassPath(this.beanClass);
        classPool.appendClassPath(beanClassPath);

        CtClass superClass = classPool.get(this.beanClass.getName());

        ctClass.setSuperclass(superClass);
        ctClass.addInterface(classPool.get(IDataContract.class.getName()));

        this.addFields(classPool, ctClass);
        this.addMethods(classPool, ctClass);
        this.overrideMethods(classPool, ctClass, superClass);

        return ctClass;
    }

    private String getDataContractClassName() {
        if (IDataContract.class.isAssignableFrom(beanClass)) {
            return beanClass.getName();
        }
        return beanClass.getName() + DATACONTRACT_CLASS_SUFFIX;
    }

    private void overrideCollectionFieldGetMethod(ClassPool classPool, CtClass ctClass, CtClass superClass,
                                                  BeanCollectionField collectionField) throws NotFoundException,
            CannotCompileException {
        String name = collectionField.getName();
        CtMethod method = superClass.getDeclaredMethod("get" + name);
        CtMethod newMethod = new CtMethod(method.getReturnType(), method.getName(), method.getParameterTypes(),
                ctClass);

        String varDataContractHelper = DataContractHelper.class.getName();
        String superMethodBody = "super.get" + name + "()";

        StringBuilder body = new StringBuilder();

        body.append("{");
        body.append("	java.util.List list = " + superMethodBody + ";");
        body.append("   if(list == null){");
        body.append("		return list;");
        body.append("   }");
        body.append("	if (" + varDataContractHelper + ".isInInitial($0)) {");
        body.append("		return list;");
        body.append("   }");
        body.append("	if (!" + varDataContractHelper + ".isReadOnly($0)) {");
        body.append("		return list;");
        body.append("   }");
        body.append("	return java.util.Collections.unmodifiableList(list);");
        body.append("}");

        newMethod.setBody(body.toString());

        ctClass.addMethod(newMethod);

    }

    private void overrideDataFieldSetMethod(ClassPool classPool, CtClass ctClass, CtClass superClass,
                                            BeanDataField dataField) throws CannotCompileException, NotFoundException {
        String name = dataField.getName();
        Method getMethod = dataField.getGetMethod();
        String getMethodBody = "$0." + getMethod.getName() + "();";

        if (dataField.isPkField()) {
            this.addMethod(classPool, ctClass, classPool.get("java.lang.Object"), "getPkValue", null,
                    "{return " + getMethodBody + "}");
        }

        Method setMethod = dataField.getSetMethod();
        CtMethod method = superClass.getDeclaredMethod(setMethod.getName());
        CtMethod newMethod = new CtMethod(method.getReturnType(), method.getName(), method.getParameterTypes(),
                ctClass);

        String DataContractHelper = DataContractHelper.class.getName();
        String Objects = Objects.class.getName();
        String superMethodBody = "super." + setMethod.getName() + "($1)";

        StringBuilder body = new StringBuilder();

        body.append("{");
        body.append("	if (" + DataContractHelper + ".isInInitial($0)) {");
        body.append("		" + superMethodBody + ";");
        body.append("		return;");
        body.append("   }");
        body.append("   " + DataContractHelper + ".assertNotReadOnly($0);");
        body.append("   Object oldValue = " + getMethodBody);
        body.append("   if (!" + Objects + ".equals(oldValue, $1)) {");
        body.append("   	" + superMethodBody + ";");
        body.append("   	" + DataContractHelper + ".recordOldValue($0, \"" + name + "\", oldValue);");
        body.append("   }");
        body.append("}");

        newMethod.setBody(body.toString());

        ctClass.addMethod(newMethod);
    }

    private void overrideMethods(ClassPool classPool, CtClass ctClass, CtClass superClass)
            throws NotFoundException, CannotCompileException {
        IDataObjectMeta objectMeta = BeanMetaManager.getBeanMeta(beanClass);
        Collection<IField> fields = objectMeta.getFields();

        for (IField field : fields) {
            if (field instanceof BeanDataField) {
                BeanDataField dataField = (BeanDataField) field;

                // 重写datafield的set方法
                this.overrideDataFieldSetMethod(classPool, ctClass, superClass, dataField);
            } else if (field instanceof BeanCollectionField) {
                BeanCollectionField collectionField = (BeanCollectionField) field;

                // 重写collectionfield的get方法
                this.overrideCollectionFieldGetMethod(classPool, ctClass, superClass, collectionField);
            }
        }

    }

}
